<?php

namespace yunj\app\admin\service\auth;

use yunj\app\admin\enum\auth\AuthPageOpen;
use yunj\app\admin\enum\auth\AuthRequestType;
use yunj\app\admin\enum\auth\AuthType;
use yunj\app\admin\enum\IsDemo;
use yunj\app\admin\enum\IsSystem;
use yunj\app\admin\enum\RedisKey;
use yunj\app\admin\enum\RedisKeyGroup;
use yunj\app\admin\enum\State;
use yunj\app\admin\service\route\RouteRequestService;
use yunj\app\admin\service\route\RouteSyncSystemDataService;
use yunj\core\Config;
use yunj\core\exception\GeneralException;

class AuthService extends Service {

    // 拥有所有权限的角色
    const HAS_ALL_AUTH_ROLES = ['administrator'];

    /**
     * 所有权限
     * @var array
     */
    private $allAuths = null;

    private static $instance;

    public static function getInstance() {
        if (!self::$instance instanceof self) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 获取所有权限
     * @param bool $auths
     * @return array
     */
    public function getAllAuths(): array {
        if (is_null($this->allAuths)) $this->setAllAuths();
        return $this->allAuths;
    }

    /**
     * 设置所有权限
     */
    public function setAllAuths(?array $auths = null): void {
        if (!is_null($auths)) {
            $this->allAuths = $auths;
            return;
        }
        /** @var RedisKey $redisKeyObj */
        $redisKeyObj = yunj_config('admin.use_demo')
            ? RedisKey::ADMIN_ALL_AUTH_DATA()
            : RedisKey::ADMIN_ALL_AUTH_DATA_BY_EXCLUDE_DEMO();
        $allAuths = $redisKeyObj->getCacheValue(86400);
        $this->allAuths = $allAuths;
    }

    /**
     * 是否拥有子权限（hasSub防止重新去循环查询是否有子权限）
     * @param string $authKey
     * @return bool 没配置默认返回有
     */
    public function isHasSub(string $authKey): bool {
        $allAuths = $this->getAllAuths();
        return isset($allAuths[$authKey]) && array_key_exists('hasSub', $allAuths[$authKey]) ? $allAuths[$authKey]['hasSub'] : true;
    }

    /**
     * 设置是否拥有子权限（hasSub防止重新去循环查询是否有子权限）
     * @param string $authKey
     * @param bool $hasSub
     */
    public function setHasSub(string $authKey, bool $hasSub): void {
        $allAuths = $this->getAllAuths();
        if (!isset($allAuths[$authKey])) return;
        $allAuths[$authKey]['hasSub'] = $hasSub;
        $this->setAllAuths($allAuths);
    }

    /**
     * 获取当前登录成员的菜单权限
     * @return array
     */
    public function getCurrMenuAuths(): array {
        static $auths;
        if ($auths) return $auths;
        $auths = [];
        $allAuths = $this->getAllAuths();
        $member = $this->getMember();
        if ($member->isHasAllAuth()) {
            $auths = $allAuths;
        } else {
            $memberOwnAllAuths = $member->getOwnAllAuths();
            foreach ($memberOwnAllAuths as $memberOwnAllAuthKey) {
                if (isset($allAuths[$memberOwnAllAuthKey])) {
                    $auths[$memberOwnAllAuthKey] = $allAuths[$memberOwnAllAuthKey];
                }
            }
        }
        $auths = array_sort($auths, 'menu_sort');
        return $auths;
    }

    /**
     * 获取当前页面面包屑html结构（仅支持tab页面）
     * @return string
     */
    public function getCurrBreadcrumbHtmlLayout(): string {
        $layout = '';
        if (strstr(yunj_config('admin.dashboard_url'), request()->baseUrl())) {
            return "<a href='javascript:void(0);'><cite>我的桌面</cite></a>";
        }
        $authKeys = $this->getCurrBreadcrumbAuthKeys();
        for ($i = 0; $i < count($authKeys); $i++) {
            $key = $authKeys[$i];
            $auth = $this->allAuths[$key];
            $itemLayout = $auth['name'];
            $isLast = $i == (count($authKeys) - 1); // 是否是最后一个
            if ($isLast) {
                $itemLayout = "<cite>{$itemLayout}</cite>";
            }
            $onclickScript = $isLast ? '' : $this->getAuthHtmlAOnClickScript($auth);
            $itemLayout = "<a href='javascript:void(0);' {$onclickScript}>{$itemLayout}</a>";
            $layout .= $itemLayout;
        }
        return $layout;
    }

    /**
     * 获取当前面包屑权限
     * @param string $authKey
     * @return array
     */
    private function getCurrBreadcrumbAuthKeys(string $authKey = ''): array {
        static $authKeys;
        $authKeys = $authKeys ?: [];
        $authKey = $authKey ?: $this->getCurrRequestAuthKey();
        if (!$authKey) return $authKeys;
        $auth = $this->getAllAuths()[$authKey];
        // 菜单或内部页面
        if (
            (($typeObj = $this->getAuthTypeObj($auth)) && $typeObj->isMenu())
            || (($pageOpenObj = $this->getAuthPageOpenObj($auth)) && $pageOpenObj->isLoaclPage())
        ) {
            array_unshift($authKeys, $authKey);
        }
        if ($auth['parent']) {
            $this->getCurrBreadcrumbAuthKeys($auth['parent']);
        }
        return $authKeys;
    }

    /**
     * 获取全局搜索所有结果的html结构
     * @return string
     */
    public function getCurrGlobalSearchResultHtmlLayout(): string {
        $layout = '';
        $currAuthKeys = $this->getMember()->getOwnAllAuths();
        $allAuths = $this->getAllAuths();

        // 相同描述的序号
        foreach ($currAuthKeys as $key) {
            $auth = $allAuths[$key] ?? [];
            if (!$auth) continue;

            $authName = $auth['name'];
            if (($parent = $auth['parent']) && $parentName = $allAuths[$parent]['name'] ?? '') {
                $authName .= "({$parentName})";
            }

            $layoutCall = null;
            $authPageOpenObj = $this->getAuthPageOpenObj($auth);
            if ($authPageOpenObj->isPage() && ($onclickScript = $this->getAuthHtmlAOnClickScript($auth))) {
                $layoutCall = function ($key, $auth) use ($onclickScript, $authName) {
                    return "<a href='javascript:void(0);' {$onclickScript} title='{$authName}'>{$authName}</a>";
                };
            }
            $itemLayout = ($layoutCall instanceof \Closure) ? call_user_func_array($layoutCall, [$key, $auth]) : '';
            if ($itemLayout) {
                $layout .= "<div class='item' data-key='{$key}' data-desc='{$authName}'>{$itemLayout}</div>";
            }
        }
        return $layout;
    }

    /**
     * 获取当前的菜单的权限树
     * @param AuthType|mixed $type 类型
     * @param int $level 等级
     * @return array
     */
    public function getCurrMenuAuthTree(AuthType $type, int $level = 1): array {
        $currAuths = $this->getCurrMenuAuths();

        $tree = [];
        foreach ($currAuths as $key => $auth) {
            if ($auth['parent'] || $auth['type'] != $type->getValue()) continue;
            $hasSub = $this->isHasSub($key);
            $level = 1;
            $sub = $hasSub ? $this->getSubMenuAuthTree($key, $level, $currAuths, function (array $auth) use ($type) {
                return $auth['type'] == $type->getValue();
            }) : [];
            $auth['level'] = $level;
            $auth['sub'] = $sub;
            $this->setHasSub($key, !!$sub);
            $tree[$key] = $auth;
        }
        return $tree;
    }

    /**
     * 获取子权限树
     * @param string $parent 父权限key
     * @param int $parentLevel 父权限等级
     * @param array $auths
     * @param callable|null $filter 过滤条件
     * @return array
     */
    private function getSubMenuAuthTree(string $parent, int $parentLevel, array $auths, ?callable $filter = null): array {
        $subAuths = [];
        foreach ($auths as $key => $auth) {
            if ($auth['parent'] != $parent) continue;
            if ($filter && !call_user_func($filter, $auth)) continue;
            // 当前等级
            $level = $parentLevel + 1;
            // 判断当前权限的子权限
            $auth['level'] = $level;
            $auth['sub'] = $this->getSubMenuAuthTree($key, $level, $auths, $filter);
            $subAuths[$key] = $auth;
        }
        return $subAuths;
    }

    /**
     * 获取当前的顶部菜单html结构（无限极）
     * @param array $auths
     * @return string
     */
    public function getCurrTopMenuHtmlLayout(array $auths = []): string {
        $layout = '';
        /** @var AuthType $type */
        $type = AuthType::TOP_MENU();
        $auths = $auths ?: $this->getCurrMenuAuthTree($type);
        foreach ($auths as $k => $v) {
            $itemLayout = $this->getAuthHtmlLayout($k, $v);
            if ($v['sub']) {
                $itemSubLayout = $this->getCurrTopMenuHtmlLayout($v['sub']);
                if ($itemSubLayout) $itemLayout .= "<ul class='top-menu-sub'>{$itemSubLayout}</ul>";
            }
            $itemLayout = "<li class='top-menu-item'>{$itemLayout}</li>";
            $layout .= $itemLayout;
        }
        return $layout;
    }

    /**
     * 获取当前的侧边栏菜单html结构（无限极）
     * @param array $auths
     * @return string
     */
    public function getCurrSidebarMenuHtmlLayout(array $auths = []): string {
        $layout = '';
        /** @var AuthType $type */
        $type = AuthType::SIDEBAR_MENU();
        $auths = $auths ?: $this->getCurrMenuAuthTree($type);
        foreach ($auths as $k => $v) {
            $itemLayout = $this->getAuthHtmlLayout($k, $v);
            if ($v['sub']) {
                $itemSubLayout = $this->getCurrSidebarMenuHtmlLayout($v['sub']);
                if ($itemSubLayout) $itemLayout .= "<ul class='sub-menu'>{$itemSubLayout}</ul>";
            }
            $itemLayout = "<li>{$itemLayout}</li>";
            $layout .= $itemLayout;
        }
        return $layout;
    }

    /**
     * 获取权限项的html结构
     * @param string $key
     * @param array $auth
     * @return string
     */
    private function getAuthHtmlLayout(string $key, array $auth): string {
        $onclickScript = $this->getAuthHtmlAOnClickScript($auth);

        if ($auth['type'] == AuthType::TOP_MENU) {
            // 顶部菜单
            $layoutCall = function ($key, $auth) use ($onclickScript) {
                $layout = $auth['name'];
                if ($iconClass = icon_class($auth['icon'])) {
                    $layout = "<i class='{$iconClass}'></i>" . $layout;
                }
                $layout = "<span class='desc'>{$layout}</span>";
                if ($auth['sub']) {
                    $_iconClass = $auth['parent'] == '' ? 'layui-icon-down' : 'layui-icon-right';
                    $layout .= "<i class='layui-icon {$_iconClass}'></i>";
                }
                $layout = "<a href='javascript:void(0);' {$onclickScript} title='{$auth['name']}'>{$layout}</a>";
                return $layout;
            };
        } elseif ($auth['type'] == AuthType::SIDEBAR_MENU) {
            // 侧边栏菜单
            $layoutCall = function ($key, $auth) use ($onclickScript) {
                $layout = "<cite>{$auth['name']}</cite>";
                if ($iconClass = icon_class($auth['icon'])) {
                    $layout = "<i class='{$iconClass} left-nav-li' lay-tips='{$auth['name']}'></i>" . $layout;
                }
                if ($auth['sub']) {
                    $layout .= "<i class='layui-icon layui-icon-left li-icon-right'></i>";
                }
                // a标签style
                $aStyle = "style='padding-left:" . (15 * $auth['level']) . "px;'";
                $layout = "<a href='javascript:void(0);' {$aStyle} {$onclickScript}>{$layout}</a>";
                return $layout;
            };
        }
        $layout = isset($layoutCall) && is_callable($layoutCall) ? call_user_func_array($layoutCall, [$key, $auth]) : '';
        return is_string($layout) ? $layout : '';
    }

    /**
     * 获取权限项a标签的onclick脚本
     * @param array $auth
     * @return string
     */
    private function getAuthHtmlAOnClickScript(array $auth): string {
        $script = '';
        // 生成url
        if ($auth['request_type'] == AuthRequestType::REQUEST_ID) {
            if (!$auth['request_id']) {
                return '';
            }
            $requestData = RouteRequestService::getValidRouteRequestDataById($auth['request_id']);
            if (!$requestData || !$requestData['route_base_url'] || ($requestData['method'] && $requestData['method'] != 'GET')) return '';
            $url = $requestData['route_base_url'];
            if (is_json($requestData['require_params'], $requireParams, true) && $requireParams) {
                $canGenerate = true;    // 是否可以生成url
                foreach ($requireParams as $k => $v) {
                    if ($v === null) {
                        $canGenerate = false;
                        break;
                    }
                }
                if (!$canGenerate) return '';
                $url .= (strstr($url, '?') ? '&' : '?') . http_build_query($requireParams);
            }
        } else {
            if (!$auth['request_url']) {
                return '';
            }
            $url = $auth['request_url'];
        }

        switch ($auth['page_open']) {
            case AuthPageOpen::TAB:
                $script = "yunj.openTab('{$url}','{$auth['name']}')";
                break;
            case AuthPageOpen::POPUP:
                $script = "yunj.openPopup('{$url}','{$auth['name']}')";
                break;
            case AuthPageOpen::NEW:
                $script = "yunj.openNewPage('{$url}')";
                break;
        }
        if ($script) $script = "onclick={$script}";
        return $script;
    }

    /**
     * 获取当前请求的权限（或指定属性值）
     * 若或取得当前请求权限不准，则完善请求、权限配置
     * 如：顶部菜单、侧边栏菜单同时指向一个路由请求项配置，则此时系统无法分辨出来自哪里
     * @param string $attr 属性
     * @param null|mixed $attrDef 属性默认值
     * @return string|array|mixed
     */
    public function getCurrRequestAuth(string $attr = '', $attrDef = null) {
        static $auth;
        if (isset($auth)) {
            return $attr ? ($auth[$attr] ?? $attrDef) : $auth;
        }
        $auth = $auth ?: [];
        if (!($requestId = request()->adminRouteRequestData['id'] ?? null)) {
            return $attr ? ($auth[$attr] ?? $attrDef) : $auth;
        }

        /** @var RedisKey $redisKeyObj */
        $redisKeyObj = RedisKey::ADMIN_AUTH_DATA_BY_REQUEST_ID();
        $auth = $redisKeyObj->setArgs($requestId)->getCacheValue(86400);

        return $attr ? ($auth[$attr] ?? $attrDef) : $auth;
    }

    /**
     * 获取当前请求的权限key
     * @return string
     */
    public function getCurrRequestAuthKey(): string {
        return $this->getCurrRequestAuth('key', '');
    }

    /**
     * 校验当前成员是否拥有指定权限
     * @param string|array|mixed $auth [权限|权限key]
     * @return bool
     */
    public function check($auth = ''): bool {
        $member = $this->getMember();
        if ($member->isHasAllAuth()) return true;
        $auth = $auth ?: $this->getCurrRequestAuthKey();
        if (
            !$auth
            || !($authKey = is_string($auth) ? $auth : ($auth['key'] ?? ''))
        ) {
            return true;
        }
        // 拥有的所有角色的权限合集
        $ownAuths = $member->getOwnAllAuths();
        // 判断是否拥有对应的请求权限（键值反转使用isset判断更快，也不用去重）
        $ownAuths = array_flip($ownAuths);
        return isset($ownAuths[$authKey]);
    }

    /**
     * 获取权限类型对象
     * @param string|array|mixed $auth
     * @return AuthType|null|mixed
     */
    private function getAuthTypeObj($auth) {
        $key = is_string($auth) ? $auth : ($auth['key'] ?? '');
        $allAuths = $this->getAllAuths();
        if (!$key || !($authData = $allAuths[$key] ?? [])) {
            return null;
        }
        if (!isset($authData['typeObj'])) {
            $authData['typeObj'] = AuthType::byValue($authData['type']);
            $allAuths[$key] = $authData;
            $this->setAllAuths($allAuths);
        }
        return $authData['typeObj'];
    }

    /**
     * 获取权限页面打开方式对象
     * @param string|array|mixed $auth
     * @return AuthPageOpen|null|mixed
     */
    private function getAuthPageOpenObj($auth) {
        $key = is_string($auth) ? $auth : ($auth['key'] ?? '');
        $allAuths = $this->getAllAuths();
        if (!$key || !($authData = $allAuths[$key] ?? [])) {
            return null;
        }
        if (!isset($authData['pageOpenObj'])) {
            $authData['pageOpenObj'] = AuthPageOpen::byValue($authData['page_open']);
            $allAuths[$key] = $authData;
            $this->setAllAuths($allAuths);
        }
        return $authData['pageOpenObj'];
    }

    /**
     * 同步系统权限
     * @throws GeneralException
     */
    public static function syncSystemDatas() {
        // 同步系统路由数据
        RouteSyncSystemDataService::handle();

        $insertDatas = [];
        $updateDatas = [];  // 包含数据更新、移入回收站、恢复正常
        $currTime = time();
        $model = self::getAdminAuthModel();
        $existKeys = $model->getColumnOptions('key', [
            ['is_system', '=', IsSystem::YES],
            ['state', '<>', State::DELETED],
        ]);

        // 不能随意修改include顺序，有排序处理
        $demoAuths = include YUNJ_VENDOR_SRC_PATH . ds_replace('app/demo/auths.php');
        $adminAuths = include YUNJ_VENDOR_SRC_PATH . ds_replace('app/admin/auths.php');
        // 避免与include文件内部变量重名
        $configAuths = $demoAuths + $adminAuths;
        //dd($configAuths);

        $sort = count($existKeys) + 1;
        $hasRequestAuths = [];    // 已使用的request_id权限数据
        foreach ($configAuths as $key => $auth) {
            $data = array_supp($auth, [
                'key' => $key,
                'parent' => '',
                'name' => '',
                'desc' => '',
                'type' => AuthType::NORMAL,
                'icon' => '',
                'page_open' => AuthPageOpen::NIL,
                'request_type' => AuthRequestType::REQUEST_ID,
                'request_id' => null,
                'request_url' => '',
                'is_system' => IsSystem::YES,
                'is_demo' => IsDemo::NO,
                'last_update_time' => $currTime,
                'state' => State::NORMAL
            ]);
            if ($authUrl = $auth['url'] ?? []) {
                $authUrl = (is_array($authUrl) ? $authUrl : [$authUrl]) + ['', '', []];
                $baseUrl = $authUrl[0] ?: '';
                $method = $authUrl[1] ?: '';
                $params = $authUrl[2] ?: [];
                if (substr($baseUrl, 0, 4) == 'http' || substr($baseUrl, 0, 2) == '//') {
                    // 外部地址
                    $data['request_type'] = AuthRequestType::REQUEST_URL;
                    $data['request_url'] = $baseUrl;
                } else {
                    // 内部地址
                    $requestDbData = RouteRequestService::getValidRouteRequestData($baseUrl, $method, $params);
                    if (!$requestDbData) {
                        throw new GeneralException("权限 {$data['name']}[{$key}]对应配置路由请求项不存在");
                    }
                    if ($hasRequestAuths && ($requestAuthData = $hasRequestAuths[$requestDbData['id']] ?? [])) {
                        $requestDesc = $requestDbData['route_desc'];
                        if ($requestDbData['desc'] && $requestDbData['desc'] != $requestDesc) {
                            $requestDesc .= ' ' . $requestDbData['desc'];
                        }
                        $error = "权限 [{$data['name']}]({$key}) 与权限 [{$requestAuthData['name']}]({$requestAuthData['key']}) 路由请求项[{$requestDesc}] 配置冲突";
                        throw new GeneralException($error);
                    }
                    $data['request_type'] = AuthRequestType::REQUEST_ID;
                    $data['request_id'] = $requestDbData['id'];
                    $hasRequestAuths[$requestDbData['id']] = $data;
                }
            }

            if (($keyIdx = array_search($key, $existKeys)) !== false) {
                $updateDatas[] = $data;
                unset($existKeys[$keyIdx]);
            } else {
                $data['sort'] = $sort;
                $data['menu_sort'] = $sort;
                $data['create_time'] = $currTime;
                $insertDatas[] = $data;
                $sort++;
            }
        }
        // 处理移入回收站的数据
        foreach ($existKeys as $existKey) {
            $updateDatas[] = [
                'key' => $existKey,
                'last_update_time' => $currTime,
                'state' => State::RECYLE_BIN,
            ];
        }

        if ($insertDatas) {
            $model->addRows($insertDatas);
        }
        if ($updateDatas) {
            $model->batchChange($updateDatas);
        }

        // 清理缓存
        self::reset();
    }

    // 重置
    public static function reset() {
        // 处理所有数据完整属性值
        self::handleAllDatasFullAttr();
        // 清理缓存
        self::clearCache();
    }

    // 清理缓存
    public static function clearCache(): void {
        /** @var RedisKeyGroup $dashboardRedisKeyGroup */
        $redisKeyGroup = RedisKeyGroup::ADMIN_AUTH();
        $redisKeyGroup->delCacheValue();
    }

    /**
     * 处理所有数据的完整属性值（补充完整名称等）
     */
    private static function handleAllDatasFullAttr() {
        $allItems = self::getAdminAuthModel()->getOwnRowsToArray([['state', '<>', State::DELETED]]);
        $datas = [];
        self::setFullAttrDatas($datas, [''], $allItems);
        if ($datas) {
            self::getAdminAuthModel()->batchChange($datas);
        }
    }

    private static function setFullAttrDatas(array &$datas, array $parents, array $allItems): void {
        $newParents = [];
        foreach ($allItems as $k => $item) {
            ['key' => $key, 'parent' => $parent, 'name' => $name] = $item;
            if (!in_array($parent, $parents, true)) {
                continue;
            }
            $parentData = $datas[$parent] ?? [];
            $parentFullName = $parentData['full_name'] ?? '';
            $datas[$key] = [
                'key' => $key,
                'full_name' => $parentFullName . $name,
            ];
            $newParents[] = $key;
        }
        if ($newParents) {
            self::setFullAttrDatas($datas, $newParents, $allItems);
        }
    }

}